/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * SortableAndSearchableTable.java
 * Copyright (C) 2010-2019 University of Waikato, Hamilton, New Zealand
 */

package adams.gui.core;

import javax.swing.ListSelectionModel;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
import java.util.Hashtable;
import java.util.Vector;

/**
 * A specialized JTable that allows double-clicking on header for resizing to
 * optimal width, as well as being searchable and sortable.
 *
 * @author  fracpete (fracpete at waikato dot ac dot nz)
 */
public class SortableAndSearchableTable
  extends BaseTable
  implements SortableTable, SearchableTable, TableWithColumnFilters {

  /** for serialization. */
  private static final long serialVersionUID = -3176811618121454828L;

  /** the key for the column width. */
  public static final String KEY_COLWIDTH = "col width";

  /** the key for the sort column setting. */
  public static final String KEY_SORTCOL = "sort col";

  /** the key for the sort order. */
  public static final String KEY_SORTORDER = "sort order";

  /** the key for case-sensitive. */
  public static final String KEY_SORTCASESENSITIVE = "sort case-sensitive";

  /** the key for the search string. */
  public static final String KEY_SEARCHSTRING = "search string";

  /** the key for the regular expression search flag. */
  public static final String KEY_SEARCHREGEXP = "search reg exp";

  /** the sortable/searchable model. */
  protected SortableAndSearchableWrapperTableModel m_Model;

  /** whether to automatically sort table models that get set via setModel. */
  protected boolean m_SortNewTableModel;

  /**
   * Constructs a default <code>SortedBaseTable</code> that is initialized with a default
   * data model, a default column model, and a default selection
   * model.
   */
  public SortableAndSearchableTable() {
    super();
  }

  /**
   * Constructs a <code>SortedBaseTable</code> with <code>numRows</code>
   * and <code>numColumns</code> of empty cells using
   * <code>DefaultTableModel</code>.  The columns will have
   * names of the form "A", "B", "C", etc.
   *
   * @param numRows           the number of rows the table holds
   * @param numColumns        the number of columns the table holds
   */
  public SortableAndSearchableTable(int numRows, int numColumns) {
    super(numRows, numColumns);
  }

  /**
   * Constructs a <code>SortedBaseTable</code> to display the values in the two dimensional array,
   * <code>rowData</code>, with column names, <code>columnNames</code>.
   * <code>rowData</code> is an array of rows, so the value of the cell at row 1,
   * column 5 can be obtained with the following code:
   * <p>
   * <pre> rowData[1][5]; </pre>
   * <p>
   * All rows must be of the same length as <code>columnNames</code>.
   * <p>
   * @param rowData           the data for the new table
   * @param columnNames       names of each column
   */
  public SortableAndSearchableTable(final Object[][] rowData, final Object[] columnNames) {
    super(rowData, columnNames);
  }

  /**
   * Constructs a <code>SortedBaseTable</code> to display the values in the
   * <code>Vector</code> of <code>Vectors</code>, <code>rowData</code>,
   * with column names, <code>columnNames</code>.  The
   * <code>Vectors</code> contained in <code>rowData</code>
   * should contain the values for that row. In other words,
   * the value of the cell at row 1, column 5 can be obtained
   * with the following code:
   * <p>
   * <pre>((Vector)rowData.elementAt(1)).elementAt(5);</pre>
   * <p>
   * @param rowData           the data for the new table
   * @param columnNames       names of each column
   */
  public SortableAndSearchableTable(Vector rowData, Vector columnNames) {
    super(rowData, columnNames);
  }

  /**
   * Constructs a <code>SortedBaseTable</code> that is initialized with
   * <code>dm</code> as the data model, a default column model,
   * and a default selection model.
   *
   * @param dm        the data model for the table
   */
  public SortableAndSearchableTable(TableModel dm) {
    super(dm);
  }

  /**
   * Constructs a <code>SortedBaseTable</code> that is initialized with
   * <code>dm</code> as the data model, <code>cm</code>
   * as the column model, and a default selection model.
   *
   * @param dm        the data model for the table
   * @param cm        the column model for the table
   */
  public SortableAndSearchableTable(TableModel dm, TableColumnModel cm) {
    super(dm, cm);
  }

  /**
   * Constructs a <code>SortedBaseTable</code> that is initialized with
   * <code>dm</code> as the data model, <code>cm</code> as the
   * column model, and <code>sm</code> as the selection model.
   * If any of the parameters are <code>null</code> this method
   * will initialize the table with the corresponding default model.
   * The <code>autoCreateColumnsFromModel</code> flag is set to false
   * if <code>cm</code> is non-null, otherwise it is set to true
   * and the column model is populated with suitable
   * <code>TableColumns</code> for the columns in <code>dm</code>.
   *
   * @param dm        the data model for the table
   * @param cm        the column model for the table
   * @param sm        the row selection model for the table
   */
  public SortableAndSearchableTable(TableModel dm, TableColumnModel cm, ListSelectionModel sm) {
    super(dm, cm, sm);
  }

  /**
   * Returns the initial setting of whether to sort new models.
   * Default implementation returns "false".
   *
   * @return		true if new models need to be sorted
   */
  protected boolean initialSortNewTableModel() {
    return false;
  }

  /**
   * Returns whether the initial sort is case-sensitive.
   *
   * @return		true if case-sensitive
   */
  protected boolean initialSortCaseSensitive() {
    return true;
  }

  /**
   * Sets whether to sort new models.
   *
   * @param value	if true then new models get sorted
   */
  public void setSortNewTableModel(boolean value) {
    m_SortNewTableModel = value;
    if (m_SortNewTableModel)
      sort(0);
  }

  /**
   * Returns whether to sort new models.
   * Default implementation is initialized with "false".
   *
   * @return		true if new models get sorted
   */
  public boolean getSortNewTableModel() {
    return m_SortNewTableModel;
  }

  /**
   * Initializes some GUI-related things.
   */
  @Override
  protected void initGUI() {
    super.initGUI();

    m_SortNewTableModel = initialSortNewTableModel();
    setCaseSensitive(initialSortCaseSensitive());
    m_Model.addMouseListenerToHeader(this);
  }

  /**
   * Finishes the initialization.
   */
  @Override
  protected void finishInit() {
    if (getSortNewTableModel())
      sort(0);
    super.finishInit();
  }

  /**
   * Returns the class of the table model that the models need to be derived
   * from. The default implementation just returns TableModel.class
   *
   * @return		the class the models must be derived from
   */
  protected Class getTableModelClass() {
    return TableModel.class;
  }

  /**
   * Backs up the settings from the old model.
   *
   * @param model	the old model (the model stored within the SortedModel)
   * @return		the backed up settings
   */
  protected Hashtable<String,Object> backupModelSettings(TableModel model) {
    Hashtable<String,Object>	result;

    result = new Hashtable<>();

    result.put(KEY_COLWIDTH, getColumnWidthApproach());
    result.put(KEY_SORTCOL, m_Model.getSortColumn());
    result.put(KEY_SORTORDER, m_Model.isAscending());
    result.put(KEY_SORTCASESENSITIVE, m_Model.isCaseSensitive());

    if (model instanceof SearchableTableModel) {
      if (((SearchableTableModel) model).getSeachString() != null)
	result.put(KEY_SEARCHSTRING, ((SearchableTableModel) model).getSeachString());
      result.put(KEY_SEARCHREGEXP, ((SearchableTableModel) model).isRegExpSearch());
    }

    return result;
  }

  /**
   * Restores the settings previously backed up.
   *
   * @param model	the new model (the model stored within the SortedModel)
   * @param settings	the old settings, null if no settings were available
   */
  protected void restoreModelSettings(TableModel model, Hashtable<String,Object> settings) {
    int			sortCol;
    boolean		asc;
    boolean		caseSensitive;
    String		search;
    boolean		regexp;
    ColumnWidthApproach	colWidth;

    // default values
    sortCol       = 0;
    asc           = true;
    caseSensitive = true;
    search        = null;
    regexp        = false;
    colWidth      = initialUseOptimalColumnWidths();

    // get stored values
    if (settings != null) {
      colWidth      = (ColumnWidthApproach) settings.get(KEY_COLWIDTH);
      sortCol       = (Integer) settings.get(KEY_SORTCOL);
      asc           = (Boolean) settings.get(KEY_SORTORDER);
      caseSensitive = (Boolean) settings.get(KEY_SORTCASESENSITIVE);

      if (model instanceof SearchableTableModel) {
	search = (String) settings.get(KEY_SEARCHSTRING);
	regexp = (Boolean) settings.get(KEY_SEARCHREGEXP);
      }
    }

    // restore sorting
    m_Model.setCaseSensitive(caseSensitive);
    if (getSortNewTableModel())
      m_Model.sort(sortCol, asc);

    // restore search
    if (model instanceof SearchableTableModel)
      ((SearchableTableModel) model).search(search, regexp);

    // set optimal column widths
    setColumnWidthApproach(colWidth);
  }

  /**
   * Sets the model to display - only {@link #getTableModelClass()}.
   *
   * @param model	the model to display
   */
  @Override
  public synchronized void setModel(TableModel model) {
    Hashtable<String,Object>	settings;

    if (!(getTableModelClass().isInstance(model)))
      model = createDefaultDataModel();

    // backup current setup
    if (m_Model != null) {
      settings = backupModelSettings(m_Model);
      getTableHeader().removeMouseListener(m_Model.getHeaderMouseListener());
    }
    else {
      settings = null;
    }

    m_Model = new SortableAndSearchableWrapperTableModel(model);
    super.setModel(m_Model);
    m_Model.addMouseListenerToHeader(this);

    // restore setup
    restoreModelSettings(m_Model, settings);
  }

  /**
   * Sets the base model to use. Discards any sorting.
   *
   * @param value       the base model
   */
  public synchronized void setUnsortedModel(TableModel value) {
    m_Model.setUnsortedModel(value);
  }

  /**
   * Sets the base model to use.
   *
   * @param value       	the base model
   * @param restoreSorting	whether to restore the sorting
   */
  public synchronized void setUnsortedModel(TableModel value, boolean restoreSorting) {
    m_Model.setUnsortedModel(value, restoreSorting);
  }

  /**
   * returns the underlying model, can be null.
   *
   * @return            the current model
   */
  public synchronized TableModel getUnsortedModel() {
    if (m_Model != null)
      return m_Model.getUnsortedModel();
    else
      return null;
  }

  /**
   * Returns the actual underlying row the given visible one represents. Useful
   * for retrieving "non-visual" data that is also stored in a TableModel.
   *
   * @param visibleRow	the displayed row to retrieve the original row for
   * @return		the original row
   */
  public synchronized int getActualRow(int visibleRow) {
    return m_Model.getActualRow(visibleRow);
  }

  /**
   * Returns the "visible" row derived from row in the actual table model.
   *
   * @param internalRow	the row in the actual model
   * @return		the row in the sorted model, -1 in case of an error
   */
  public synchronized int getDisplayRow(int internalRow) {
    return m_Model.getDisplayRow(internalRow);
  }

  /**
   * returns whether the table was sorted.
   *
   * @return        true if the table was sorted
   */
  public synchronized boolean isSorted() {
    return m_Model.isSorted();
  }

  /**
   * Returns the sort column.
   *
   * @return		the sort column
   */
  public synchronized int getSortColumn() {
    return m_Model.getSortColumn();
  }

  /**
   * Returns whether sorting is ascending or not.
   *
   * @return		true if ascending
   * @see		#isSorted()
   * @see		#getSortColumn()
   */
  public synchronized boolean isAscending() {
    return m_Model.isAscending();
  }

  /**
   * Sets whether the sorting is case-sensitive.
   *
   * @param value	true if case-sensitive
   */
  public void setCaseSensitive(boolean value) {
    m_Model.setCaseSensitive(value);
  }

  /**
   * Returns whether the sorting is case-sensitive.
   *
   * @return		true if case-sensitive
   */
  public boolean isCaseSensitive() {
    return m_Model.isCaseSensitive();
  }

  /**
   * sorts the table over the given column (ascending).
   *
   * @param columnIndex     the column to sort over
   */
  public synchronized void sort(int columnIndex) {
    if (m_Model != null)
      m_Model.sort(columnIndex);
  }

  /**
   * sorts the table over the given column, either ascending or descending.
   *
   * @param columnIndex     the column to sort over
   * @param ascending       ascending if true, otherwise descending
   */
  public synchronized void sort(int columnIndex, boolean ascending) {
    if (m_Model != null)
      m_Model.sort(columnIndex, ascending);
  }

  /**
   * Returns the actual row count in the model.
   *
   * @return		the row count in the underlying data
   */
  public synchronized int getActualRowCount() {
    return m_Model.getActualRowCount();
  }

  /**
   * Performs a search for the given string. Limits the display of rows to
   * ones containing the search string.
   *
   * @param searchString	the string to search for
   * @param regexp		whether to perform regular expression matching
   * 				or just plain string comparison
   */
  public synchronized void search(String searchString, boolean regexp) {
    int[]	selected;
    int		i;
    int		index;

    // determine actual selected rows
    selected = getSelectedRows();
    for (i = 0; i < selected.length; i++)
      selected[i] = getActualRow(selected[i]);

    m_Model.search(searchString, regexp);

    // re-select rows that are still in current search
    clearSelection();
    for (i = 0; i < selected.length; i++) {
      index = getDisplayRow(selected[i]);
      if (index != -1)
	getSelectionModel().addSelectionInterval(index, index);
    }
  }

  /**
   * Returns the current search string.
   *
   * @return		the search string, null if not filtered
   */
  public synchronized String getSeachString() {
    return m_Model.getSeachString();
  }

  /**
   * Returns whether the last search was a regular expression based one.
   *
   * @return		true if last search was a reg exp one
   */
  public synchronized boolean isRegExpSearch() {
    return m_Model.isRegExpSearch();
  }

  /**
   * Sets the filter for the column.
   *
   * @param column	the column to filter
   * @param filter	the filter string
   * @param isRegExp	whether the filter is a regular expression
   */
  public void setColumnFilter(int column, String filter, boolean isRegExp) {
    m_Model.setColumnFilter(column, filter, isRegExp);
  }

  /**
   * Returns the filter for the column.
   *
   * @param column	the column to query
   * @return		the filter, null if none present
   */
  public String getColumnFilter(int column) {
    return m_Model.getColumnFilter(column);
  }

  /**
   * Returns the whether the filter for the column is a regular expression.
   *
   * @param column	the column to query
   * @return		true if filter set and regular expression
   */
  public boolean isColumnFilterRegExp(int column) {
    return m_Model.isColumnFilterRegExp(column);
  }

  /**
   * Returns the whether there is a filter active for the column.
   *
   * @param column	the column to query
   * @return		true if a filter is active
   */
  public boolean isColumnFiltered(int column) {
    return m_Model.isColumnFiltered(column);
  }

  /**
   * Returns whether there is at least one filter active.
   *
   * @return		true if at least one filter is active
   */
  public boolean isAnyColumnFiltered() {
    return m_Model.isAnyColumnFiltered();
  }

  /**
   * Removes any filter for the column.
   *
   * @param column	the column to update
   */
  public void removeColumnFilter(int column) {
    m_Model.removeColumnFilter(column);
  }

  /**
   * Removes all column filters
   */
  public void removeAllColumnFilters() {
    m_Model.removeAllColumnFilters();
  }
}
